Coverage Report

Created: 2026-04-26 08:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\src\daemon\mod.rs
Line
Count
Source
1
//! Daemon imlementation
2
3
#![deny(clippy::implicit_return)]
4
#![allow(clippy::needless_return, clippy::doc_overindented_list_items)]
5
#![warn(missing_docs)]
6
7
use std::cmp::max;
8
use std::collections::HashMap;
9
use std::{
10
    io,
11
    sync::{Arc, Mutex},
12
    time::Duration,
13
};
14
use std::{thread, time};
15
16
use crate::get_console_window_handle;
17
use crate::utils::config::{Cluster, DaemonConfig};
18
use crate::utils::debug::StringRepr;
19
use crate::utils::windows::{clear_screen, set_console_color, WindowsApi};
20
use crate::{
21
    serde::{
22
        deserialization::deserialize_pid, serialization::serialize_input_record_0,
23
        SERIALIZED_INPUT_RECORD_0_LENGTH, SERIALIZED_PID_LENGTH,
24
    },
25
    spawn_console_process,
26
    utils::{
27
        constants::{PIPE_NAME, PKG_NAME},
28
        windows::{
29
            arrange_console, get_console_input_buffer, read_keyboard_input,
30
            set_console_border_color,
31
        },
32
    },
33
    WindowsSettingsDefaultTerminalApplicationGuard,
34
};
35
use bracoxide::explode;
36
use log::{debug, error, warn};
37
use tokio::sync::broadcast::error::TryRecvError;
38
use tokio::{
39
    net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions},
40
    sync::broadcast::{self, Receiver, Sender},
41
    task::JoinHandle,
42
};
43
use windows::Win32::System::Console::{
44
    CONSOLE_CHARACTER_ATTRIBUTES, INPUT_RECORD_0, LEFT_CTRL_PRESSED, RIGHT_CTRL_PRESSED,
45
};
46
47
use windows::Win32::UI::Input::KeyboardAndMouse::{
48
    VIRTUAL_KEY, VK_A, VK_C, VK_E, VK_ESCAPE, VK_H, VK_R, VK_T,
49
};
50
use windows::Win32::UI::WindowsAndMessaging::{SW_RESTORE, SW_SHOWMINIMIZED};
51
use windows::Win32::{
52
    Foundation::{COLORREF, HANDLE, HWND, STILL_ACTIVE},
53
    System::{Console::ENABLE_PROCESSED_INPUT, Threading::PROCESS_QUERY_INFORMATION},
54
};
55
56
use self::workspace::WorkspaceArea;
57
58
mod workspace;
59
60
/// The capacity of the broadcast channel used
61
/// to send the input records read from the console input buffer
62
/// to the named pipe servers connected to each client in parallel.
63
const SENDER_CAPACITY: usize = 1024 * 1024;
64
65
/// Runtime state of a client's assigned pipe server task.
66
///
67
/// Observed by the pipe server on each input record; determines whether
68
/// the record is forwarded to the client over the named pipe.
69
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70
enum PipeServerState {
71
    /// Forward all input records to the client.
72
    Enabled,
73
}
74
75
/// Representation of a client
76
#[derive(Clone)]
77
struct Client {
78
    /// Hostname the client is connect to (or supposed to connect to).
79
    hostname: String,
80
    /// Window handle to the clients console window.
81
    window_handle: HWND,
82
    /// Process handle to the client process.
83
    process_handle: HANDLE,
84
    /// Process id of the client process.
85
    ///
86
    /// Used by the pipe server task to correlate which client has connected
87
    /// to it, via a handshake over the named pipe.
88
    process_id: u32,
89
    /// Shared state between this client and its assigned pipe server task.
90
    ///
91
    /// Populated at [`Client`] construction; cloned by the pipe server task upon
92
    /// successful PID correlation and consulted during input forwarding to
93
    /// determine whether records should be sent to the client.
94
    pipe_server_state: Arc<Mutex<PipeServerState>>,
95
}
96
97
unsafe impl Send for Client {}
98
99
/// Collection of [`Client`]s maintaining insertion order and a PID-indexed
100
/// lookup table.
101
///
102
/// The ordered list preserves client window placement semantics, while the
103
/// index enables O(1) lookup by process id — required by the pipe server task
104
/// during PID correlation and future per-client pipe server control.
105
struct Clients {
106
    /// Ordered list of clients; order matches launch order and is used for
107
    /// window arrangement and z-order synchronization.
108
    list: Vec<Client>,
109
    /// Maps a client's process id to its index in [`list`](Clients::list).
110
    pid_index: HashMap<u32, usize>,
111
}
112
113
impl Clients {
114
    /// Creates a new empty collection.
115
6
    fn new() -> Self {
116
6
        return Clients {
117
6
            list: Vec::new(),
118
6
            pid_index: HashMap::new(),
119
6
        };
120
6
    }
121
122
    /// Appends a client to the collection and records its position in the
123
    /// PID index.
124
    ///
125
    /// # Arguments
126
    ///
127
    /// * `client` - The [`Client`] to add.
128
    ///
129
    /// # Panics
130
    ///
131
    /// Panics if a client with the same process id is already present, as
132
    /// duplicate PIDs indicate broken daemon bookkeeping.
133
9
    fn push(&mut self, client: Client) {
134
9
        let index = self.list.len();
135
9
        assert!(
136
9
            !self.pid_index.contains_key(&client.process_id),
137
            "Duplicate client PID {} — daemon bookkeeping broken",
138
            client.process_id,
139
        );
140
8
        self.pid_index.insert(client.process_id, index);
141
8
        self.list.push(client);
142
8
    }
143
144
    /// Returns a reference to the client with the given process id, if any.
145
    ///
146
    /// # Arguments
147
    ///
148
    /// * `pid` - The process id of the client to look up.
149
    ///
150
    /// # Returns
151
    ///
152
    /// `Some(&Client)` if a client with the given PID exists, `None` otherwise.
153
10
    fn get_by_pid(&self, pid: u32) -> Option<&Client> {
154
10
        return self
155
10
            .pid_index
156
10
            .get(&pid)
157
10
            .map(|&index| return 
&self.list[index]7
);
158
10
    }
159
160
    /// Retains only the clients for which the predicate returns `true`,
161
    /// rebuilding the PID index to reflect the new positions.
162
    ///
163
    /// # Arguments
164
    ///
165
    /// * `f` - Predicate applied to each [`Client`]; kept when it returns `true`.
166
1
    fn retain<F: FnMut(&Client) -> bool>(&mut self, mut f: F) {
167
3
        
self.list1
.
retain1
(|client| return f(client));
168
1
        self.pid_index.clear();
169
2
        for (index, client) in 
self.list.iter()1
.
enumerate1
() {
170
2
            self.pid_index.insert(client.process_id, index);
171
2
        }
172
1
    }
173
}
174
175
/// Allows treating a [`Clients`] collection as a `&[Client]`, so callers can
176
/// use `&clients` where a slice is expected and get slice methods
177
/// (`iter`, `len`, `is_empty`, ...) via deref coercion.
178
impl std::ops::Deref for Clients {
179
    type Target = [Client];
180
181
7
    fn deref(&self) -> &[Client] {
182
7
        return &self.list;
183
7
    }
184
}
185
186
/// Consumes the collection and yields its clients in insertion order.
187
///
188
/// Used when merging a freshly launched [`Clients`] batch into an existing
189
/// collection while also spawning per-client pipe servers.
190
impl IntoIterator for Clients {
191
    type Item = Client;
192
    type IntoIter = std::vec::IntoIter<Client>;
193
194
0
    fn into_iter(self) -> Self::IntoIter {
195
0
        return self.list.into_iter();
196
0
    }
197
}
198
199
/// Hacky wrapper around a window handle.
200
///
201
/// As we cannot implement foreign traits for foreign structs
202
/// we introduce this wrapper to implement [Send] for [HWND].
203
#[derive(Debug, Eq)]
204
struct HWNDWrapper {
205
    hwdn: HWND,
206
}
207
208
unsafe impl Send for HWNDWrapper {}
209
210
impl PartialEq for HWNDWrapper {
211
    /// Returns whether to `HWNDWrapper` instances are equal or not
212
    /// based on the [HWND] they wrap.
213
2
    fn eq(&self, other: &Self) -> bool {
214
2
        return self.hwdn == other.hwdn;
215
2
    }
216
}
217
218
/// Returns a window handle to the current console window.
219
///
220
/// The [HWND] is wrapped in a `HWNDWrapper` so that
221
/// we can pass it inbetween threads.
222
0
fn get_console_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper {
223
0
    return HWNDWrapper {
224
0
        hwdn: api.get_console_window(),
225
0
    };
226
0
}
227
228
/// Returns a window handle to the foreground window.
229
///
230
/// The [HWND] is wrapped in a `HWNDWrapper` so that
231
/// we can pass it inbetween threads.
232
0
fn get_foreground_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper {
233
0
    return HWNDWrapper {
234
0
        hwdn: api.get_foreground_window(),
235
0
    };
236
0
}
237
238
/// Enum of all possible control mode states.
239
#[derive(PartialEq, Debug)]
240
enum ControlModeState {
241
    /// Controle mode is inactive.
242
    Inactive,
243
    /// One of the keys required for the control mode key combination
244
    /// is currently being pressed.
245
    Initiated,
246
    /// All required keys for the control mode key combination were pressed
247
    /// and control mode is now active.
248
    ///
249
    /// Active control mode prevents any input records from being sent to clients.
250
    Active,
251
}
252
253
/// The daemon is responsible to launch a client for
254
/// each host, positioning the client windows, forwarding
255
/// input records to all clients and handling control mode.
256
struct Daemon<'a> {
257
    /// A list of hostnames to connect to.
258
    hosts: Vec<String>,
259
    /// A username to use to connect to all clients.
260
    ///
261
    /// If it is empty the clients will use the SSH config to find an approriate
262
    /// username.
263
    username: Option<String>,
264
    /// Optional port used for all SSH connections.
265
    port: Option<u16>,
266
    /// The `DaemonConfig` that controls how the daemon console window looks like.
267
    config: &'a DaemonConfig,
268
    /// List of available cluster tags
269
    clusters: &'a [Cluster],
270
    /// The current control mode state.
271
    control_mode_state: ControlModeState,
272
    /// If debug mode is enabled on the daemon it will also be enabled on all
273
    /// clients.
274
    debug: bool,
275
}
276
277
impl<'a> Daemon<'a> {
278
    /// Launches all client windows and blocks on the main run loop.
279
    ///
280
    /// Sets up the daemon console by disabling processed input mode and applying
281
    /// the configured colors and dimensions.
282
    /// Once all client windows have successfully started the daemon console window
283
    /// is moved to the foreground and receives focus.
284
0
    async fn launch<W: WindowsApi + Clone + 'static>(mut self, windows_api: &W) {
285
0
        windows_api
286
0
            .set_console_title(format!("{PKG_NAME} daemon").as_str())
287
0
            .unwrap();
288
0
        set_console_color(
289
0
            windows_api,
290
0
            CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color),
291
        );
292
0
        set_console_border_color(windows_api, COLORREF(0x000000FF));
293
294
0
        toggle_processed_input_mode(windows_api); // Disable processed input mode
295
296
        // Initialize the COM library so we can use UI automation
297
0
        windows_api
298
0
            .initialize_com_library(windows::Win32::System::Com::COINIT_MULTITHREADED)
299
0
            .unwrap();
300
301
0
        let workspace_area = workspace::get_workspace_area(windows_api, self.config.height);
302
303
0
        self.arrange_daemon_console(windows_api, &workspace_area);
304
305
        // Looks like on windows 10 re-arranging the console resets the console output buffer
306
0
        set_console_color(
307
0
            windows_api,
308
0
            CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color),
309
        );
310
311
0
        let mut clients = Arc::new(Mutex::new(
312
0
            launch_clients(
313
0
                windows_api,
314
0
                self.hosts.to_vec(),
315
0
                &self.username,
316
0
                self.port,
317
0
                self.debug,
318
0
                &workspace_area,
319
0
                self.config.aspect_ratio_adjustement,
320
0
                0,
321
0
            )
322
0
            .await,
323
        ));
324
325
        // Now that all clients started, focus the daemon console again.
326
0
        let daemon_console = windows_api.get_console_window();
327
0
        let _ = windows_api.set_foreground_window(daemon_console);
328
0
        let _ = windows_api.focus_window_with_automation(daemon_console);
329
330
0
        self.print_instructions(windows_api);
331
0
        self.run(windows_api, &mut clients, &workspace_area).await;
332
0
    }
333
334
    /// The main run loop of the `daemon` subcommand.
335
    ///
336
    /// Opens a multi-producer, multi-consumer broadcasting channel used to
337
    /// send the read input records in parallel to the name pipe servers
338
    /// the clients are listening on.
339
    /// Spawns a background thread that waits for all clients to terminate
340
    /// and then stops the current process.
341
    /// Spawns a background thread that ensures the z-order of all client
342
    /// windows is in sync with the daemon window.
343
    /// I.e. if the daemon window is focussed, all clients should be moved to the foreground.
344
    ///
345
    /// The main loop consists of waiting for input records to read from the keyboard,
346
    /// sending them to all clients and handling control mode.
347
    ///
348
    /// # Arguments
349
    ///
350
    /// * `windows_api`                     - The Windows API implementation to use
351
    /// * `clients`                         - A thread safe mapping from the number
352
    ///                                       a client console window was launched at
353
    ///                                       in relation to the other client windows
354
    ///                                       and the clients console window handle.
355
    /// * `workspace_area`                  - The available workspace area on the
356
    ///                                       primary monitor minus the space occupied
357
    ///                                       by the daemon console window.
358
0
    async fn run<W: WindowsApi + Clone + 'static>(
359
0
        &mut self,
360
0
        windows_api: &W,
361
0
        clients: &mut Arc<Mutex<Clients>>,
362
0
        workspace_area: &workspace::WorkspaceArea,
363
0
    ) {
364
0
        let (sender, _) =
365
0
            broadcast::channel::<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>(SENDER_CAPACITY);
366
367
0
        let mut servers = Arc::new(Mutex::new(
368
0
            self.launch_named_pipe_servers(&sender, Arc::clone(clients)),
369
        ));
370
371
        // Monitor client processes
372
0
        let clients_clone = Arc::clone(clients);
373
0
        let windows_api_clone = windows_api.clone();
374
0
        tokio::spawn(async move {
375
            loop {
376
0
                clients_clone.lock().unwrap().retain(|client| {
377
0
                    match windows_api_clone.get_exit_code(client.process_handle) {
378
0
                        Ok(exit_code) => return exit_code == STILL_ACTIVE.0 as u32,
379
0
                        Err(_) => return false, // Process handle is invalid, remove client
380
                    }
381
0
                });
382
0
                if clients_clone.lock().unwrap().is_empty() {
383
                    // All clients have exited, exit the daemon as well
384
0
                    std::process::exit(0);
385
0
                }
386
0
                tokio::time::sleep(Duration::from_millis(5)).await;
387
            }
388
        });
389
390
0
        ensure_client_z_order_in_sync_with_daemon(
391
0
            Arc::new(windows_api.clone()),
392
0
            clients.to_owned(),
393
        );
394
395
        loop {
396
0
            self.handle_input_record(
397
0
                windows_api,
398
0
                &sender,
399
0
                read_keyboard_input(windows_api),
400
0
                clients,
401
0
                workspace_area,
402
0
                &mut servers,
403
0
            )
404
0
            .await;
405
        }
406
    }
407
408
    /// Launch a named pipe server for each host in a dedicated thread.
409
    ///
410
    /// # Arguments
411
    ///
412
    /// * `sender` - The sender end of the broadcast channel through which
413
    ///              the main thread will send the input records that are to
414
    ///              be forwarded to the clients.
415
    ///
416
    /// # Returns
417
    ///
418
    /// Returns a list of [JoinHandle]s, one handle for each thread.
419
0
    fn launch_named_pipe_servers(
420
0
        &self,
421
0
        sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
422
0
        clients: Arc<Mutex<Clients>>,
423
0
    ) -> Vec<JoinHandle<()>> {
424
0
        let mut servers: Vec<JoinHandle<()>> = Vec::new();
425
0
        for _ in &self.hosts {
426
0
            self.launch_named_pipe_server(&mut servers, sender, Arc::clone(&clients));
427
0
        }
428
0
        return servers;
429
0
    }
430
431
    /// Launch a named pipe server in a dedicated thread.
432
    ///
433
    /// # Arguments
434
    ///
435
    /// * `servers` - A list of [JoinHandle]s to which the join handle for
436
    ///               the new thread will be added.
437
    /// * `sender`  - The sender end of the broadcast channel through which
438
    ///               the main thread will send the input records that are to
439
    ///               be forwarded to the clients.
440
0
    fn launch_named_pipe_server(
441
0
        &self,
442
0
        servers: &mut Vec<JoinHandle<()>>,
443
0
        sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
444
0
        clients: Arc<Mutex<Clients>>,
445
0
    ) {
446
0
        let named_pipe_server = ServerOptions::new()
447
0
            .access_inbound(true)
448
0
            .access_outbound(true)
449
0
            .pipe_mode(PipeMode::Message)
450
0
            .create(PIPE_NAME)
451
0
            .unwrap_or_else(|err| {
452
0
                error!("{}", err);
453
0
                panic!("Failed to create named pipe server",)
454
            });
455
0
        let mut receiver = sender.subscribe();
456
0
        servers.push(tokio::spawn(async move {
457
0
            named_pipe_server_routine(named_pipe_server, &mut receiver, clients).await;
458
0
        }));
459
0
    }
460
461
    /// Handle the given input record.
462
    ///
463
    /// Input records are being forwarded to all clients.
464
    /// If a sequence of input records matches the control mode
465
    /// key combination, forwarding is temporarily interrupted,
466
    /// until control mode is exited.
467
    ///
468
    /// # Arguments
469
    ///
470
    /// * `sender`                          - The sender end of the broadcast channel
471
    ///                                       through which we will send the input records
472
    ///                                       that are being forwarded to the clients
473
    ///                                       by the named pipe servers (`servers`).
474
    /// * `input_record`                    - The [INPUT_RECORD_0].`KeyEvent` read from the
475
    ///                                       console input buffer.
476
    /// * `clients`                         - A thread safe mapping from the number
477
    ///                                       a client console window was launched at
478
    ///                                       in relation to the other client windows
479
    ///                                       and the clients console window handle.
480
    ///                                       The mapping will be extended if additional clients
481
    ///                                       are being added through control mode `[c]reate window(s)`.
482
    /// * `workspace_area`                  - The available workspace area on the
483
    ///                                       primary monitor minus the space occupied
484
    ///                                       by the daemon console window.
485
    /// * `servers`                         - A thread safe list of [JoinHandle]s,
486
    ///                                       one handle for each named pipe server background thread.
487
    ///                                       The list will be extended if additional clients are being added
488
    ///                                       through control mode `[c]reate window(s)`.
489
0
    async fn handle_input_record<W: WindowsApi + Clone + 'static>(
490
0
        &mut self,
491
0
        windows_api: &W,
492
0
        sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
493
0
        input_record: INPUT_RECORD_0,
494
0
        clients: &mut Arc<Mutex<Clients>>,
495
0
        workspace_area: &workspace::WorkspaceArea,
496
0
        servers: &mut Arc<Mutex<Vec<JoinHandle<()>>>>,
497
0
    ) {
498
0
        if self.control_mode_is_active(windows_api, input_record) {
499
0
            if self.control_mode_state == ControlModeState::Initiated {
500
0
                clear_screen(windows_api);
501
0
                println!("Control Mode (Esc to exit)");
502
0
                println!("[c]reate window(s), [r]etile, copy active [h]ostname(s)");
503
0
                self.control_mode_state = ControlModeState::Active;
504
0
                return;
505
0
            }
506
0
            let key_event = unsafe { input_record.KeyEvent };
507
0
            if !key_event.bKeyDown.as_bool() {
508
0
                return;
509
0
            }
510
0
            match (
511
0
                VIRTUAL_KEY(key_event.wVirtualKeyCode),
512
0
                key_event.dwControlKeyState,
513
0
            ) {
514
0
                (VK_R, 0) => {
515
0
                    self.rearrange_client_windows(
516
0
                        windows_api,
517
0
                        &clients.lock().unwrap(),
518
0
                        workspace_area,
519
0
                    );
520
0
                    self.arrange_daemon_console(windows_api, workspace_area);
521
0
                }
522
0
                (VK_E, 0) => {
523
0
                    // TODO: Select windows
524
0
                }
525
0
                (VK_T, 0) => {
526
0
                    // TODO: trigger input on selected windows
527
0
                }
528
                (VK_C, 0) => {
529
0
                    clear_screen(windows_api);
530
                    // TODO: make ESC abort
531
0
                    println!("Hostname(s) or cluster tag(s): (leave empty to abort)");
532
0
                    toggle_processed_input_mode(windows_api); // As it was disabled before, this enables it again
533
0
                    let mut hostnames = String::new();
534
0
                    match io::stdin().read_line(&mut hostnames) {
535
0
                        Ok(2) => {
536
0
                            // Empty input (only newline '\n')
537
0
                        }
538
                        Ok(_) => {
539
0
                            let number_of_existing_clients = clients.lock().unwrap().len();
540
0
                            let new_clients = launch_clients(
541
0
                                windows_api,
542
0
                                resolve_cluster_tags(
543
0
                                    hostnames.split(' ').map(|x| return x.trim()).collect(),
544
0
                                    self.clusters,
545
                                )
546
0
                                .into_iter()
547
0
                                .map(|x| return x.to_owned())
548
0
                                .collect(),
549
0
                                &self.username,
550
0
                                self.port,
551
0
                                self.debug,
552
0
                                workspace_area,
553
0
                                self.config.aspect_ratio_adjustement,
554
0
                                number_of_existing_clients,
555
                            )
556
0
                            .await;
557
0
                            for client in new_clients.into_iter() {
558
0
                                clients.lock().unwrap().push(client);
559
0
                                self.launch_named_pipe_server(
560
0
                                    &mut servers.lock().unwrap(),
561
0
                                    sender,
562
0
                                    Arc::clone(clients),
563
0
                                );
564
0
                            }
565
                        }
566
0
                        Err(error) => {
567
0
                            error!("{error}");
568
                        }
569
                    }
570
0
                    toggle_processed_input_mode(windows_api); // Re-disable processed input mode.
571
0
                    self.rearrange_client_windows(
572
0
                        windows_api,
573
0
                        &clients.lock().unwrap(),
574
0
                        workspace_area,
575
                    );
576
0
                    self.arrange_daemon_console(windows_api, workspace_area);
577
                    // Focus the daemon console again.
578
0
                    let daemon_window = windows_api.get_console_window();
579
0
                    let _ = windows_api.set_foreground_window(daemon_window);
580
0
                    let _ = windows_api.focus_window_with_automation(daemon_window);
581
0
                    self.quit_control_mode(windows_api);
582
                }
583
                (VK_H, 0) => {
584
0
                    let mut active_hostnames: Vec<String> = vec![];
585
0
                    for client in clients.lock().unwrap().iter() {
586
0
                        if windows_api.is_window(client.window_handle) {
587
0
                            active_hostnames.push(client.hostname.clone());
588
0
                        }
589
                    }
590
0
                    cli_clipboard::set_contents(active_hostnames.join(" ")).unwrap();
591
0
                    self.quit_control_mode(windows_api);
592
                }
593
0
                _ => {}
594
            }
595
0
            return;
596
0
        }
597
0
        let error_handler = |err| {
598
0
            error!("{}", err);
599
0
            panic!(
600
                "Failed to serialize input recored `{}`",
601
0
                input_record.string_repr()
602
            )
603
        };
604
0
        match sender.send(
605
0
            serialize_input_record_0(&input_record)[..]
606
0
                .try_into()
607
0
                .unwrap_or_else(error_handler),
608
0
        ) {
609
0
            Ok(_) => {}
610
0
            Err(_) => {
611
0
                thread::sleep(time::Duration::from_nanos(1));
612
0
            }
613
        }
614
0
    }
615
616
    /// Returns whether control mode is active or not given the input_record.
617
    ///
618
    /// For control mode to be active this function needs to be called
619
    /// multiple times, as a key press translates to an input record and
620
    /// the key combination that activates control mode has 2 keys:
621
    /// `Ctrl + A`.
622
    /// The current control mode state is stored in `self.control_mode_state`.
623
    ///
624
    /// # Arguments
625
    ///
626
    /// * `windows_api` - The Windows API implementation to use
627
    /// * `input_record` -  A KeyEvent input record.
628
    ///
629
    /// # Returns
630
    ///
631
    /// Whether or not control mode is active.
632
0
    fn control_mode_is_active<W: WindowsApi>(
633
0
        &mut self,
634
0
        windows_api: &W,
635
0
        input_record: INPUT_RECORD_0,
636
0
    ) -> bool {
637
0
        let key_event = unsafe { input_record.KeyEvent };
638
0
        if self.control_mode_state == ControlModeState::Active {
639
0
            if key_event.wVirtualKeyCode == VK_ESCAPE.0 {
640
0
                self.quit_control_mode(windows_api);
641
0
                return false;
642
0
            }
643
0
            return true;
644
0
        }
645
0
        if (key_event.dwControlKeyState & LEFT_CTRL_PRESSED >= 1
646
0
            || key_event.dwControlKeyState & RIGHT_CTRL_PRESSED >= 1)
647
0
            && key_event.wVirtualKeyCode == VK_A.0
648
        {
649
0
            self.control_mode_state = ControlModeState::Initiated;
650
0
            return true;
651
0
        }
652
0
        return false;
653
0
    }
654
655
    /// Prints the default daemon instructions to the daemon console and
656
    /// sets `self.control_mode_state` to inactive.
657
0
    fn quit_control_mode<W: WindowsApi>(&mut self, windows_api: &W) {
658
0
        self.print_instructions(windows_api);
659
0
        self.control_mode_state = ControlModeState::Inactive;
660
0
    }
661
662
    /// Clears the console screen and prints the default daemon instructions.
663
0
    fn print_instructions<W: WindowsApi>(&self, windows_api: &W) {
664
0
        clear_screen(windows_api);
665
0
        println!("Input to terminal: (Ctrl-A to enter control mode)");
666
0
    }
667
668
    /// Iterates over all still open client windows and re-arranges them
669
    /// on the screen based on the aspect ration adjustment daemon configuration.
670
    ///
671
    /// Client windows will be re-sized and re-positioned.
672
    ///
673
    /// # Arguments
674
    ///
675
    /// * `windows_api`                     - The Windows API implementation to use
676
    /// * `clients`                         - A thread safe mapping from the number
677
    ///                                       a client console window was launched at
678
    ///                                       in relation to the other client windows
679
    ///                                       and the clients console window handle.
680
    ///                                       The number is relevant to determine the
681
    ///                                       position on the screen the window should
682
    ///                                       be placed at.
683
    /// * `workspace_area`                  - The available workspace area on the
684
    ///                                       primary monitor minus the space occupied
685
    ///                                       by the daemon console window.
686
0
    fn rearrange_client_windows<W: WindowsApi>(
687
0
        &self,
688
0
        windows_api: &W,
689
0
        clients: &[Client],
690
0
        workspace_area: &workspace::WorkspaceArea,
691
0
    ) {
692
0
        let mut valid_clients = Vec::new();
693
0
        for client in clients.iter() {
694
0
            let exit_code = match windows_api.get_exit_code(client.process_handle) {
695
0
                Ok(code) => code,
696
0
                Err(_) => continue, // Process handle is invalid, skip client
697
            };
698
0
            if exit_code == STILL_ACTIVE.0 as u32 && windows_api.is_window(client.window_handle) {
699
0
                valid_clients.push(client);
700
0
            }
701
        }
702
0
        for (index, client) in valid_clients.iter().enumerate() {
703
0
            arrange_client_window(
704
0
                windows_api,
705
0
                &client.window_handle,
706
0
                workspace_area,
707
0
                index,
708
0
                valid_clients.len(),
709
0
                self.config.aspect_ratio_adjustement,
710
            )
711
        }
712
0
    }
713
714
    /// Re-sizes and re-positions the daemon console window on the screen
715
    /// based on the daemon height configuration.
716
    ///
717
    /// # Arguments
718
    ///
719
    /// * `windows_api` - The Windows API implementation to use
720
    /// * `workspace_area` - The available workspace area on the
721
    ///                      primary monitor minus the space occupied
722
    ///                      by the daemon console window.
723
0
    fn arrange_daemon_console<W: WindowsApi>(
724
0
        &self,
725
0
        windows_api: &W,
726
0
        workspace_area: &WorkspaceArea,
727
0
    ) {
728
0
        let (x, y, width, height) = get_console_rect(
729
0
            0,
730
0
            workspace_area.height,
731
0
            workspace_area.width - (workspace_area.x_fixed_frame + workspace_area.x_size_frame),
732
0
            self.config.height,
733
0
            workspace_area,
734
0
        );
735
0
        arrange_console(windows_api, x, y, width, height);
736
0
    }
737
}
738
739
/// The processed console input mode controls whether special key combinations
740
/// such as `Ctrl + c` or `Ctrl + BREAK` receive special handling or are treated
741
/// as simple key presses.
742
///
743
/// By default processed input mode is enabled, meaning `Ctrl + c` is treated as
744
/// a signal, not key presses.
745
///
746
/// <https://learn.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>
747
///
748
/// # Arguments
749
///
750
/// * `windows_api` - The Windows API implementation to use
751
0
fn toggle_processed_input_mode<W: WindowsApi>(windows_api: &W) {
752
0
    let handle = get_console_input_buffer();
753
0
    let mode = windows_api.get_console_mode(handle).unwrap();
754
0
    let new_mode = windows::Win32::System::Console::CONSOLE_MODE(mode.0 ^ ENABLE_PROCESSED_INPUT.0);
755
0
    let _ = windows_api.set_console_mode(handle, new_mode);
756
0
}
757
758
/// Resolve cluster tags into hostnames
759
///
760
/// Iterates over the list of hosts to find and resolve cluster tags.
761
/// Nested cluster tags are supported but recursivness is not checked for.
762
///
763
/// # Arguments
764
///
765
/// * `hosts`       - List of hosts including hostnames and or cluster tags
766
/// * `clusters`    - List of available cluster tags
767
///
768
/// # Returns
769
///
770
/// A list of hostnames
771
12
pub fn resolve_cluster_tags<'a>(hosts: Vec<&'a str>, clusters: &'a [Cluster]) -> Vec<&'a str> {
772
12
    let mut resolved_hosts: Vec<&str> = Vec::new();
773
    let mut is_cluster_tag: bool;
774
22
    for host in 
hosts12
{
775
22
        is_cluster_tag = false;
776
22
        for 
cluster17
in clusters {
777
17
            if host == cluster.name {
778
3
                is_cluster_tag = true;
779
3
                resolved_hosts.extend(resolve_cluster_tags(
780
6
                    
cluster.hosts.iter()3
.
map3
(|host| return &**host).
collect3
(),
781
3
                    clusters,
782
                ));
783
3
                break;
784
14
            }
785
        }
786
22
        if !is_cluster_tag {
787
19
            resolved_hosts.push(host);
788
19
        
}3
789
    }
790
12
    return resolved_hosts;
791
12
}
792
793
/// Launches a client console for each given host and waits for
794
/// the client windows to exist before returning their handles.
795
///
796
/// # Arguments
797
///
798
/// * `windows_api`             - The Windows API implementation to use
799
/// * `hosts`                   - List of hosts
800
/// * `username`                - Optional username, if none is given
801
///                               the client will use the SSH config to
802
///                               determine a username.
803
/// * `port`                    - Optional port for SSH connections
804
/// * `debug`                   - Toggles debug mode on the client.
805
/// * `workspace_area`          - The available workspace area on the primary monitor
806
///                               minus the space occupied by the daemon console window.
807
///                               Used to arrange the client window.
808
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
809
///                               Used to arrange the client window.
810
/// * `index_offset`            - Offset used to position the new windows correctly
811
///                               from the start, avoiding flickering.
812
///
813
/// # Returns
814
///
815
/// A [`Clients`] collection preserving the launch order and indexed by
816
/// process id for pipe-server correlation.
817
0
async fn launch_clients<W: WindowsApi + 'static + Clone>(
818
0
    windows_api: &W,
819
0
    hosts: Vec<String>,
820
0
    username: &Option<String>,
821
0
    port: Option<u16>,
822
0
    debug: bool,
823
0
    workspace_area: &workspace::WorkspaceArea,
824
0
    aspect_ratio_adjustment: f64,
825
0
    index_offset: usize,
826
0
) -> Clients {
827
0
    let len_hosts = hosts.len();
828
0
    let _guard = WindowsSettingsDefaultTerminalApplicationGuard::new();
829
830
    // Create an Arc to share the windows_api across parallel tasks
831
0
    let windows_api_arc = Arc::new(windows_api.clone());
832
833
    // Create tasks for each client launch using spawn_blocking to handle the synchronous operations
834
0
    let mut tasks = Vec::new();
835
836
0
    for (index, host) in hosts.into_iter().enumerate() {
837
0
        let username_client = username.clone();
838
0
        let workspace_area_client = *workspace_area;
839
0
        let windows_api_clone = Arc::clone(&windows_api_arc);
840
841
        // Use spawn_blocking to run the synchronous launch_client_console in parallel
842
0
        let task = tokio::task::spawn_blocking(move || {
843
0
            let (window_handle, process_handle, process_id) = launch_client_console(
844
0
                windows_api_clone.as_ref(),
845
0
                &host,
846
0
                username_client,
847
0
                port,
848
0
                debug,
849
0
                index + index_offset,
850
0
                &workspace_area_client,
851
0
                len_hosts + index_offset,
852
0
                aspect_ratio_adjustment,
853
0
            );
854
0
            return (
855
0
                index,
856
0
                Client {
857
0
                    hostname: host,
858
0
                    window_handle,
859
0
                    process_handle,
860
0
                    process_id,
861
0
                    pipe_server_state: Arc::new(Mutex::new(PipeServerState::Enabled)),
862
0
                },
863
0
            );
864
0
        });
865
866
0
        tasks.push(task);
867
    }
868
869
    // Wait for all tasks to complete in parallel
870
0
    let mut results = Vec::new();
871
0
    for task in tasks {
872
0
        match task.await {
873
0
            Ok(result) => results.push(result),
874
0
            Err(e) => panic!("Failed to launch client: {e}"),
875
        }
876
    }
877
878
    // Sort results by index to maintain order
879
0
    results.sort_by_key(|(index, _)| return *index);
880
881
0
    let mut clients = Clients::new();
882
0
    for (_, client) in results.into_iter() {
883
0
        clients.push(client);
884
0
    }
885
0
    return clients;
886
0
}
887
888
/// Launchs a `client` console process with its own window with the given
889
/// CLI arguments/options: `host`, `username`, `port`, `debug`.
890
///
891
/// Waits for the window to open, then re-arranges it based on
892
/// the total number of clients, the size of the daemon console window and
893
/// its index relative to the other client windows.
894
///
895
/// # Arguments
896
///
897
/// * `windows_api`             - The Windows API implementation to use
898
/// * `host`                    - Hostname the client should connect to
899
/// * `username`                - Username the client should use
900
/// * `port`                    - Optional port for SSH connections
901
/// * `debug`                   - Toggle debug mode on the client
902
/// * `index`                   - The index of the client in the list of all clients.
903
///                               Used to re-arrange the client window.
904
/// * `workspace_area`          - The available workspace area on the primary monitor
905
///                               minus the space occupied by the daemon console window.
906
/// * `number_of_consoles`      - The total number of active client console windows.
907
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
908
///
909
/// # Returns
910
///
911
/// A tuple containing the window handle, process handle, and process id of the
912
/// client process.
913
0
fn launch_client_console<W: WindowsApi>(
914
0
    windows_api: &W,
915
0
    host: &str,
916
0
    username: Option<String>,
917
0
    port: Option<u16>,
918
0
    debug: bool,
919
0
    index: usize,
920
0
    workspace_area: &workspace::WorkspaceArea,
921
0
    number_of_consoles: usize,
922
0
    aspect_ratio_adjustment: f64,
923
0
) -> (HWND, HANDLE, u32) {
924
    // The first argument must be `--` to ensure all following arguments are treated
925
    // as positional arguments and not as options if they start with `-`.
926
0
    let mut client_args: Vec<String> = Vec::new();
927
0
    if debug {
928
0
        client_args.push("-d".to_string());
929
0
    }
930
0
    let mut actual_host = host;
931
0
    let mut actual_username = username;
932
0
    if let Some(split_result) = host.split_once("@") {
933
0
        actual_username = Some(split_result.0.to_owned());
934
0
        actual_host = split_result.1;
935
0
    }
936
0
    if let Some(actual_username) = actual_username.as_deref() {
937
0
        client_args.extend(vec!["-u".to_string(), actual_username.to_string()]);
938
0
    }
939
0
    if let Some(port) = port {
940
0
        client_args.extend(vec!["-p".to_string(), port.to_string()]);
941
0
    }
942
0
    client_args.push("client".to_string());
943
0
    client_args.extend(vec!["--".to_string(), actual_host.to_string()]);
944
945
0
    let process_info = spawn_console_process(windows_api, &format!("{PKG_NAME}.exe"), client_args)
946
0
        .expect("Failed to create process");
947
0
    let client_window_handle = get_console_window_handle(windows_api, process_info.dwProcessId);
948
0
    let process_handle = windows_api
949
0
        .open_process(PROCESS_QUERY_INFORMATION.0, false, process_info.dwProcessId)
950
0
        .unwrap_or_else(|err| {
951
0
            panic!(
952
                "Failed to open process handle for process {}: {}",
953
                process_info.dwProcessId, err
954
            );
955
        });
956
957
0
    arrange_client_window(
958
0
        windows_api,
959
0
        &client_window_handle,
960
0
        workspace_area,
961
0
        index,
962
0
        number_of_consoles,
963
0
        aspect_ratio_adjustment,
964
    );
965
0
    return (
966
0
        client_window_handle,
967
0
        process_handle,
968
0
        process_info.dwProcessId,
969
0
    );
970
0
}
971
972
/// Wait for the named pipe server to connect, correlate the client by
973
/// its process id, then forward serialized input records read from the
974
/// broadcast channel to the named pipe server.
975
///
976
/// Correlation: after [`NamedPipeServer::connect`] resolves, the client is
977
/// expected to write its 4 byte little-endian process id into the pipe. The
978
/// routine looks up the [`Client`] with that PID in the daemon's `clients`
979
/// collection; if it is not found, the routine logs an error and terminates
980
/// the daemon — an unknown PID indicates broken daemon bookkeeping and is
981
/// unrecoverable.
982
///
983
/// Forwarding: on every broadcast record, the routine matches on the
984
/// [`PipeServerState`] cloned from the correlated client; only
985
/// [`PipeServerState::Enabled`] writes the record to the pipe. The keep-alive
986
/// write stays unconditional so dead pipes are detected regardless of state.
987
///
988
/// If writing to the pipe fails the pipe is considered closed and the routine ends.
989
/// To detect if a client is still alive even if we are currently
990
/// not sending data, we send a "keep alive packet",
991
/// [`SERIALIZED_INPUT_RECORD_0_LENGTH`] bytes of `1`s. If that fails, the routine ends.
992
///
993
/// # Arguments
994
///
995
/// * `server`   - The named pipe server over which we send data to the
996
///                client.
997
/// * `receiver` - The receiving end of the broadcast channel through
998
///                which we get the serialize input records from the main
999
///                thread that are to be sent to the client via the named
1000
///                pipe.
1001
/// * `clients`  - The daemon's collection of tracked clients, used to
1002
///                correlate the connecting client by PID and to obtain
1003
///                the shared [`PipeServerState`] reference for this server.
1004
///
1005
/// # Panics
1006
///
1007
/// Panics if the connecting client sends a PID that is not present in
1008
/// `clients`.
1009
4
async fn named_pipe_server_routine(
1010
4
    server: NamedPipeServer,
1011
4
    receiver: &mut Receiver<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
1012
4
    clients: Arc<Mutex<Clients>>,
1013
4
) {
1014
    // wait for a client to connect
1015
4
    server.connect().await.unwrap_or_else(|err| 
{0
1016
0
        error!("{}", err);
1017
0
        panic!("Timed out waiting for clients to connect to named pipe server",)
1018
    });
1019
1020
    // Correlate the connecting client by reading its 4 byte PID.
1021
4
    let 
pid3
= read_client_pid(&server).await;
1022
3
    let 
pipe_server_state2
= match clients.lock().unwrap().get_by_pid(pid) {
1023
2
        Some(client) => Arc::clone(&client.pipe_server_state),
1024
        None => {
1025
1
            error!(
1026
                "Named pipe server received unknown PID {} — daemon bookkeeping broken",
1027
                pid
1028
            );
1029
            // In production this exits the daemon; in tests process::exit would kill
1030
            // the test runner, so we panic instead so tokio::spawn can catch it.
1031
            #[cfg(not(test))]
1032
            std::process::exit(1);
1033
            #[cfg(test)]
1034
1
            panic!("Unknown client PID {} — daemon bookkeeping broken", pid);
1035
        }
1036
    };
1037
1038
    loop {
1039
11
        let 
ser_input_record8
= match receiver.try_recv() {
1040
8
            Ok(val) => val,
1041
            Err(TryRecvError::Empty) => {
1042
2
                tokio::time::sleep(Duration::from_millis(5)).await;
1043
                // Try sending dummy data to detect early if the pipe is closed because the client exited
1044
2
                match server.try_write(&[u8::MAX; SERIALIZED_INPUT_RECORD_0_LENGTH]) {
1045
2
                    Ok(_) => continue,
1046
0
                    Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue,
1047
                    Err(_) => {
1048
0
                        debug!(
1049
                            "Named pipe server ({:?}) is closed, stopping named pipe server routine",
1050
                            server
1051
                        );
1052
0
                        return;
1053
                    }
1054
                }
1055
            }
1056
1
            Err(err) => {
1057
1
                error!("{}", err);
1058
1
                panic!("Failed to receive data from the Receiver");
1059
            }
1060
        };
1061
        // Only forward to the client if its pipe server state allows it.
1062
8
        match *pipe_server_state.lock().unwrap() {
1063
8
            PipeServerState::Enabled => {}
1064
        }
1065
        loop {
1066
14
            server.writable().await.unwrap_or_else(|err| 
{0
1067
0
                error!("{}", err);
1068
0
                panic!("Timed out waiting for named pipe server to become writable",)
1069
            });
1070
14
            match server.try_write(&ser_input_record) {
1071
                Ok(SERIALIZED_INPUT_RECORD_0_LENGTH) => {
1072
7
                    debug!("Successfully written all data");
1073
7
                    break;
1074
                }
1075
0
                Ok(n) => {
1076
                    // The data was only written partially, try again
1077
0
                    warn!(
1078
                        "Partially written data, expected {} but only wrote {}",
1079
                        SERIALIZED_INPUT_RECORD_0_LENGTH, n
1080
                    );
1081
0
                    continue;
1082
                }
1083
7
                Err(
e6
) if e.kind() == io::ErrorKind::WouldBloc
k6
=> {
1084
                    // Try again
1085
6
                    debug!("Writing to named pipe server would have blocked");
1086
6
                    continue;
1087
                }
1088
                Err(_) => {
1089
                    // Can happen if the pipe is closed because the
1090
                    // client exited
1091
1
                    debug!(
1092
                        "Named pipe server ({:?}) is closed, stopping named pipe server routine",
1093
                        server
1094
                    );
1095
1
                    return;
1096
                }
1097
            }
1098
        }
1099
    }
1100
1
}
1101
1102
/// Read the connecting client's 4 byte little-endian process id from the pipe.
1103
///
1104
/// Reads exactly 4 bytes from `server`, retrying on `WouldBlock`, and decodes
1105
/// them as a `u32`. Any non-recoverable I/O error panics, as a client that
1106
/// cannot send its PID cannot be correlated and forwarding would be
1107
/// impossible.
1108
///
1109
/// # Arguments
1110
///
1111
/// * `server` - The connected named pipe server to read from.
1112
///
1113
/// # Returns
1114
///
1115
/// The process id sent by the client.
1116
///
1117
/// # Panics
1118
///
1119
/// Panics if the pipe is closed before 4 bytes can be read, or if any
1120
/// non-`WouldBlock` I/O error occurs.
1121
4
async fn read_client_pid(server: &NamedPipeServer) -> u32 {
1122
4
    let mut buf = [0u8; SERIALIZED_PID_LENGTH];
1123
4
    let mut read = 0usize;
1124
7
    while read < SERIALIZED_PID_LENGTH {
1125
4
        server.readable().await.unwrap_or_else(|err| 
{0
1126
0
            panic!("Named pipe server is not readable for PID handshake: {err}")
1127
        });
1128
4
        match server.try_read(&mut buf[read..]) {
1129
            Ok(0) => {
1130
1
                panic!("Named pipe server closed before PID handshake completed");
1131
            }
1132
3
            Ok(n) => {
1133
3
                read += n;
1134
3
            }
1135
0
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
1136
0
                continue;
1137
            }
1138
0
            Err(e) => {
1139
0
                panic!("Failed to read PID from named pipe client: {e}");
1140
            }
1141
        }
1142
    }
1143
3
    return deserialize_pid(&buf);
1144
3
}
1145
1146
/// Re-sizes and re-positions the given client window based on the total number of clients,
1147
/// the size of the daemon console window and its index relative to the other client windows.
1148
///
1149
/// # Arguments
1150
///
1151
/// * `windows_api`              - The Windows API implementation to use
1152
/// * `handle`                   - Reference the windows handle of a client console window.
1153
/// * `workspace_area`           - The available workspace area on the primary monitor
1154
///                                minus the space occupied by the daemon console window.
1155
/// * `index`                    - The index of the client in the list of all clients.
1156
/// * `number_of_consoles`       - The total number of active client console windows.
1157
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
1158
0
fn arrange_client_window<W: WindowsApi>(
1159
0
    windows_api: &W,
1160
0
    handle: &HWND,
1161
0
    workspace_area: &workspace::WorkspaceArea,
1162
0
    index: usize,
1163
0
    number_of_consoles: usize,
1164
0
    aspect_ratio_adjustment: f64,
1165
0
) {
1166
0
    let (x, y, width, height) = determine_client_spatial_attributes(
1167
0
        index as i32,
1168
0
        number_of_consoles as i32,
1169
0
        workspace_area,
1170
0
        aspect_ratio_adjustment,
1171
0
    );
1172
    // Since windows update 10.0.19041.5072 it can happen that a client windows rendering is broken
1173
    // after a move+resize. Why is unclear, but resizing again does solve the issue.
1174
    // We first make the window 1 pixel in each dimension too small and imediately fix it.
1175
    // To reduce overhead we do not repaint the window the first time.
1176
0
    windows_api
1177
0
        .move_window(*handle, x, y, width - 1, height - 1, false)
1178
0
        .unwrap_or_else(|err| {
1179
0
            error!("{}", err);
1180
0
            panic!("Failed to move window",)
1181
        });
1182
0
    windows_api
1183
0
        .move_window(*handle, x, y, width, height, true)
1184
0
        .unwrap_or_else(|err| {
1185
0
            error!("{}", err);
1186
0
            panic!("Failed to move window",)
1187
        });
1188
0
}
1189
1190
/// Calculates the position and dimensions for a client window given its index,
1191
/// the total number of clients and the `aspect_ratio_adjustment` daemon configuration.
1192
///
1193
/// # Arguments
1194
///
1195
/// * `index`                    - The index of the client in the list of all clients.
1196
/// * `number_of_consoles`       - The total number of active client console windows.
1197
/// * `workspace_area`           - The available workspace area on the primary monitor
1198
///                                minus the space occupied by the daemon console window.
1199
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
1200
///     * `> 0.0` - Aims for vertical rectangle shape.
1201
///       The larger the value, the more exaggerated the "verticality".
1202
///       Eventually the windows will all be columns.
1203
///     * `= 0.0` - Aims for square shape.
1204
///     * `< 0.0` - Aims for horizontal rectangle shape.
1205
///       The smaller the value, the more exaggerated the "horizontality".
1206
///       Eventually the windows will all be rows.
1207
///       `-1.0` is the sweetspot for mostly preserving a 16:9 ratio.
1208
0
fn determine_client_spatial_attributes(
1209
0
    index: i32,
1210
0
    number_of_consoles: i32,
1211
0
    workspace_area: &workspace::WorkspaceArea,
1212
0
    aspect_ratio_adjustment: f64,
1213
0
) -> (i32, i32, i32, i32) {
1214
0
    let aspect_ratio = (workspace_area.width
1215
0
        + (workspace_area.x_fixed_frame + workspace_area.x_size_frame) * 2)
1216
0
        as f64
1217
0
        / (workspace_area.height + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * 2)
1218
0
            as f64;
1219
1220
0
    let grid_columns = max(
1221
0
        ((number_of_consoles as f64).sqrt() * (aspect_ratio + aspect_ratio_adjustment)) as i32,
1222
        1,
1223
    );
1224
0
    let grid_rows = max(
1225
0
        (number_of_consoles as f64 / grid_columns as f64).ceil() as i32,
1226
        1,
1227
    );
1228
1229
0
    let grid_column_index = index % grid_columns;
1230
0
    let grid_row_index = index / grid_columns;
1231
1232
0
    let is_last_row = grid_row_index == grid_rows - 1;
1233
0
    let last_row_console_count = number_of_consoles % grid_columns;
1234
1235
0
    let console_width = if is_last_row && last_row_console_count != 0 {
1236
0
        (workspace_area.width / last_row_console_count)
1237
0
            + if last_row_console_count > 1 {
1238
0
                workspace_area.x_fixed_frame + workspace_area.x_size_frame
1239
            } else {
1240
0
                0
1241
            }
1242
    } else {
1243
0
        (workspace_area.width / grid_columns)
1244
0
            + (workspace_area.x_fixed_frame + workspace_area.x_size_frame)
1245
    };
1246
1247
0
    let console_height = (workspace_area.height
1248
0
        + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * grid_row_index)
1249
0
        / grid_rows;
1250
1251
0
    let x = grid_column_index * console_width
1252
0
        - ((workspace_area.x_fixed_frame + workspace_area.x_size_frame) * (grid_column_index + 1));
1253
0
    let y = grid_row_index * console_height
1254
0
        - ((workspace_area.y_fixed_frame + workspace_area.y_size_frame) * (grid_row_index - 1));
1255
1256
0
    return get_console_rect(x, y, console_width, console_height, workspace_area);
1257
0
}
1258
1259
/// Transform the position and dimensions of a console window based
1260
/// on the workspace area.
1261
///
1262
/// To minimize empty space between windows, width and height must be adjusted
1263
/// by the `fixed_frame` and `size_frame` values.
1264
///
1265
/// # Arguments
1266
///
1267
/// * `x`              - The `x` coordinate of the window.
1268
/// * `y`              - The `y` coordinate of the window.
1269
/// * `width`          - The `width` in pixels of the window.
1270
/// * `height`         - The `height` in pixels of the window.
1271
/// * `workspace_area` - The available workspace area on the primary monitor minus
1272
///                      the space occupied by the daemon console window.
1273
///
1274
/// # Returns
1275
///
1276
/// (`x`, `y`, `width`, `height`)
1277
///
1278
0
fn get_console_rect(
1279
0
    x: i32,
1280
0
    y: i32,
1281
0
    width: i32,
1282
0
    height: i32,
1283
0
    workspace_area: &workspace::WorkspaceArea,
1284
0
) -> (i32, i32, i32, i32) {
1285
0
    return (
1286
0
        std::cmp::max(
1287
0
            workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame),
1288
0
            workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame) + x,
1289
0
        ),
1290
0
        workspace_area.y - (workspace_area.y_fixed_frame + workspace_area.y_size_frame) + y,
1291
0
        std::cmp::min(workspace_area.width, width),
1292
0
        height,
1293
0
    );
1294
0
}
1295
1296
/// Spawns a background thread that ensures the z-order of all client
1297
/// windows is in sync with the daemon window.
1298
/// I.e. if the daemon window is focussed, all clients should be moved to the foreground.
1299
///
1300
/// # Arguments
1301
///
1302
/// * `windows_api` - Arc-wrapped Windows API implementation for thread-safe access
1303
/// * `clients`     - A thread safe mapping from the number
1304
///                   a client console window was launched at
1305
///                   in relation to the other client windows
1306
///                   and the clients console window handle.
1307
///                   The mapping must be thread safe to allow
1308
///                   it to be modified by the main thread
1309
///                   while we periodically read from it in the
1310
///                   background thread.
1311
0
fn ensure_client_z_order_in_sync_with_daemon<W: WindowsApi + Send + Sync + 'static>(
1312
0
    windows_api: Arc<W>,
1313
0
    clients: Arc<Mutex<Clients>>,
1314
0
) {
1315
0
    tokio::spawn(async move {
1316
0
        let daemon_handle = get_console_window_wrapper(windows_api.as_ref());
1317
0
        let mut previous_foreground_window = get_foreground_window_wrapper(windows_api.as_ref());
1318
        loop {
1319
0
            tokio::time::sleep(Duration::from_millis(1)).await;
1320
0
            let foreground_window = get_foreground_window_wrapper(windows_api.as_ref());
1321
0
            if previous_foreground_window == foreground_window {
1322
0
                continue;
1323
0
            }
1324
0
            if foreground_window == daemon_handle
1325
0
                && !clients.lock().unwrap().iter().any(|client| {
1326
0
                    return client.window_handle == previous_foreground_window.hwdn
1327
0
                        || client.window_handle == daemon_handle.hwdn;
1328
0
                })
1329
0
            {
1330
0
                defer_windows(
1331
0
                    windows_api.as_ref(),
1332
0
                    &clients.lock().unwrap(),
1333
0
                    &daemon_handle.hwdn,
1334
0
                );
1335
0
            }
1336
0
            previous_foreground_window = foreground_window;
1337
        }
1338
    });
1339
0
}
1340
1341
/// Move all given windows to the foreground.
1342
///
1343
/// Restores minimized windows.
1344
/// If a window handle no longer points to a valid window, it is skipped.
1345
/// The daemon window is deferred last and receives focus.
1346
///
1347
/// # Arguments
1348
///
1349
/// * `windows_api`                   - The Windows API implementation to use
1350
/// * `clients`                       - A thread safe mapping from the number
1351
///                                     a client console window was launched at
1352
///                                     in relation to the other client windows
1353
///                                     and the clients console window handle.
1354
/// * `daemon_handle`                 - Handle to the daemon console window.
1355
0
fn defer_windows<W: WindowsApi>(windows_api: &W, clients: &[Client], daemon_handle: &HWND) {
1356
0
    for client in clients.iter().chain([&Client {
1357
0
        hostname: "root".to_owned(),
1358
0
        window_handle: *daemon_handle,
1359
0
        process_handle: HANDLE::default(),
1360
0
        process_id: 0,
1361
0
        pipe_server_state: Arc::new(Mutex::new(PipeServerState::Enabled)),
1362
0
    }]) {
1363
0
        let placement = match windows_api.get_window_placement(client.window_handle) {
1364
0
            Ok(placement) => placement,
1365
            Err(_) => {
1366
0
                continue;
1367
            }
1368
        };
1369
        // First restore if window is minimized
1370
0
        if placement.showCmd == SW_SHOWMINIMIZED.0.try_into().unwrap() {
1371
0
            let _ = windows_api.show_window(client.window_handle, SW_RESTORE);
1372
0
        }
1373
        // Then bring it to front using UI automation
1374
0
        let _ = windows_api.focus_window_with_automation(client.window_handle);
1375
    }
1376
0
}
1377
1378
/// The entrypoint for the `daemon` subcommand.
1379
///
1380
/// Spawns 1 client process with its own window for each host
1381
/// and 1 worker thread that handles communication with the client
1382
/// over a named pipe.
1383
/// Responsible for client window positioning and sizing.
1384
/// Handles control mode.
1385
/// Main thread reads input records from the console input buffer
1386
/// and propagates them via the background threads to all clients
1387
/// simultaneously.
1388
///
1389
/// # Arguments
1390
///
1391
/// * `windows_api` - The Windows API implementation to use
1392
/// * `hosts`    - List of hostnames for which to launch clients.
1393
/// * `username` - Username used to connect to the hosts.
1394
///                If none, each client will use the SSH config to determine
1395
///                a suitable username for their respective host.
1396
/// * `port`     - Optional port used for all SSH connections.
1397
/// * `config`   - The `DaemonConfig`.
1398
/// * `debug`    - Enables debug logging
1399
0
pub async fn main<W: WindowsApi + Clone + 'static>(
1400
0
    windows_api: &W,
1401
0
    hosts: Vec<String>,
1402
0
    username: Option<String>,
1403
0
    port: Option<u16>,
1404
0
    config: &DaemonConfig,
1405
0
    clusters: &[Cluster],
1406
0
    debug: bool,
1407
0
) {
1408
0
    let daemon: Daemon = Daemon {
1409
0
        hosts: explode(&hosts.join(" ")).unwrap_or(hosts),
1410
0
        username,
1411
0
        port,
1412
0
        config,
1413
0
        clusters,
1414
0
        control_mode_state: ControlModeState::Inactive,
1415
0
        debug,
1416
0
    };
1417
0
    daemon.launch(windows_api).await;
1418
0
    debug!("Actually exiting");
1419
0
}
1420
1421
#[cfg(test)]
1422
#[path = "../tests/daemon/test_mod.rs"]
1423
mod test_mod;